/*
 * Copyright (C) 2013 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using UnityEngine.SocialPlatforms;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using GooglePlayGames.BasicApi;
using GooglePlayGames.OurUtils;
using GooglePlayGames.BasicApi.Multiplayer;

namespace GooglePlayGames {
    /// <summary>
    /// Provides access to the Google Play Games platform. This is an implementation of
    /// UnityEngine.SocialPlatforms.ISocialPlatform. Activate this platform by calling
    /// the <see cref="Activate" /> method, then authenticate by calling
    /// the <see cref="Authenticate" /> method. After authentication
    /// completes, you may call the other methods of this class. This is not a complete
    /// implementation of the ISocialPlatform interface. Methods lacking an implementation
    /// or whose behavior is at variance with the standard are noted as such.
    /// </summary>
    public class PlayGamesPlatform : ISocialPlatform {
        private static PlayGamesPlatform sInstance = null;
        private PlayGamesLocalUser mLocalUser = null;
        private IPlayGamesClient mClient = null;

        // the default leaderboard we show on ShowLeaderboardUI
        private string mDefaultLbUi = null;

        // achievement/leaderboard ID mapping table
        private Dictionary<string, string> mIdMap = new Dictionary<string, string>();

        private PlayGamesPlatform() {
            mLocalUser = new PlayGamesLocalUser(this);
        }

        /// <summary>
        /// Gets the singleton instance of the Play Games platform.
        /// </summary>
        /// <returns>
        /// The instance.
        /// </returns>
        public static PlayGamesPlatform Instance {
            get {
                if (sInstance == null) {
                    sInstance = new PlayGamesPlatform();
                }
                return sInstance;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether debug logs are enabled. This property
        /// may be set before calling <see cref="Activate" /> method.
        /// </summary>
        /// <returns>
        /// <c>true</c> if debug log enabled; otherwise, <c>false</c>.
        /// </returns>
        public static bool DebugLogEnabled {
            get {
                return Logger.DebugLogEnabled;
            }
            set {
                Logger.DebugLogEnabled = value;
            }
        }

        /// Gets the real time multiplayer API object
        public IRealTimeMultiplayerClient RealTime {
            get {
                return mClient.GetRtmpClient();
            }
        }

        /// Gets the turn based multiplayer API object
        public ITurnBasedMultiplayerClient TurnBased {
            get {
                return mClient.GetTbmpClient();
            }
        }

        /// <summary>
        /// Activates the Play Games platform as the implementation of Social.Active.
        /// After calling this method, you can call methods on Social.Active. For
        /// example, <c>Social.Active.Authenticate()</c>.
        /// </summary>
        /// <returns>The singleton <see cref="PlayGamesPlatform" /> instance.</returns>
        public static PlayGamesPlatform Activate() {
            Logger.d("Activating PlayGamesPlatform.");
            Social.Active = PlayGamesPlatform.Instance;
            Logger.d("PlayGamesPlatform activated: " + Social.Active);
            return PlayGamesPlatform.Instance;
        }

        /// <summary>
        /// Specifies that the ID <c>fromId</c> should be implicitly replaced by <c>toId</c>
        /// on any calls that take a leaderboard or achievement ID. After a mapping is
        /// registered, you can use <c>fromId</c> instead of <c>toId</c> when making a call.
        /// For example, the following two snippets are equivalent:
        ///
        /// <code>
        /// ReportProgress("Cfiwjew894_AQ", 100.0, callback);
        /// </code>
        /// ...is equivalent to:
        /// <code>
        /// AddIdMapping("super-combo", "Cfiwjew894_AQ");
        /// ReportProgress("super-combo", 100.0, callback);
        /// </code>
        ///
        /// </summary>
        /// <param name='fromId'>
        /// The identifier to map.
        /// </param>
        /// <param name='toId'>
        /// The identifier that <c>fromId</c> will be mapped to.
        /// </param>
        public void AddIdMapping(string fromId, string toId) {
            mIdMap[fromId] = toId;
        }

        /// <summary>
        /// Authenticate the local user with the Google Play Games service.
        /// </summary>
        /// <param name='localUser'>
        /// The local user. Must be <c>Social.localUser</c>.
        /// </param>
        /// <param name='callback'>
        /// The callback to call when authentication finishes. It will be called
        /// with <c>true</c> if authentication was successful, <c>false</c>
        /// otherwise.
        /// </param>
        public void Authenticate(Action<bool> callback) {
            Authenticate(callback, false);
        }

        /// <summary>
        /// Authenticate the local user with the Google Play Games service.
        /// </summary>
        /// <param name='callback'>
        /// The callback to call when authentication finishes. It will be called
        /// with <c>true</c> if authentication was successful, <c>false</c>
        /// otherwise.
        /// </param>
        /// <param name='silent'>
        /// Indicates whether authentication should be silent. If <c>false</c>,
        /// authentication may show popups and interact with the user to obtain
        /// authorization. If <c>true</c>, there will be no popups or interaction with
        /// the user, and the authentication will fail instead if such interaction
        /// is required. A typical pattern is to try silent authentication on startup
        /// and, if that fails, present the user with a "Sign in" button that then
        /// triggers normal (not silent) authentication.
        /// </param>
        public void Authenticate(Action<bool> callback, bool silent) {
            // make a platform-specific Play Games client
            if (mClient == null) {
                Logger.d("Creating platform-specific Play Games client.");
                mClient = PlayGamesClientFactory.GetPlatformPlayGamesClient();
            }

            // authenticate!
            mClient.Authenticate(callback, silent);
        }
        
        /// <summary>
        /// Same as <see cref="Authenticate(Action<bool>,bool)"/>. Provided for compatibility
        /// with ISocialPlatform.
        /// </summary>
        /// <param name="unused">Unused.</param>
        /// <param name="callback">Callback.</param>
        public void Authenticate(ILocalUser unused, Action<bool> callback) {
            Authenticate(callback, false);
        }

        /// <summary>
        /// Determines whether the user is authenticated.
        /// </summary>
        /// <returns>
        /// <c>true</c> if the user is authenticated; otherwise, <c>false</c>.
        /// </returns>
        public bool IsAuthenticated() {
            return mClient != null && mClient.IsAuthenticated();
        }

        /// Sign out. After signing out, Authenticate must be called again to sign back in.
        public void SignOut() {
            if (mClient != null) {
                mClient.SignOut();
            }
        }

        /// <summary>
        /// Not implemented yet. Calls the callback with an empty list.
        /// </summary>
        public void LoadUsers(string[] userIDs, Action<IUserProfile[]> callback) {
            Logger.w("PlayGamesPlatform.LoadUsers is not implemented.");
            if (callback != null) {
                callback.Invoke(new IUserProfile[0]);
            }
        }

        /// <summary>
        /// Returns the user's Google ID.
        /// </summary>
        /// <returns>
        /// The user's Google ID. No guarantees are made as to the meaning or format of
        /// this identifier except that it is unique to the user who is signed in.
        /// </returns>
        public string GetUserId() {
            if (!IsAuthenticated()) {
                Logger.e("GetUserId() can only be called after authentication.");
                return "0";
            }
            return mClient.GetUserId();
        }

        /// <summary>
        /// Returns the user's display name.
        /// </summary>
        /// <returns>
        /// The user display name (e.g. "Bruno Oliveira")
        /// </returns>
        public string GetUserDisplayName() {
            if (!IsAuthenticated()) {
                Logger.e("GetUserDisplayName can only be called after authentication.");
                return "";
            }
            return mClient.GetUserDisplayName();
        }

        /// <summary>
        /// Reports the progress of an achievement (reveal, unlock or increment). This method attempts
        /// to implement the expected behavior of ISocialPlatform.ReportProgress as closely as possible,
        /// as described below. Although this method works with incremental achievements for compatibility
        /// purposes, calling this method for incremental achievements is not recommended,
        /// since the Play Games API exposes incremental achievements in a very different way
        /// than the interface presented by ISocialPlatform.ReportProgress. The implementation of this
        /// method for incremental achievements attempts to produce the correct result, but may be
        /// imprecise. If possible, call <see cref="IncrementAchievement" /> instead.
        /// </summary>
        /// <param name='achievementID'>
        /// The ID of the achievement to unlock, reveal or increment. This can be a raw Google Play
        /// Games achievement ID (alphanumeric string), or an alias that was previously configured
        /// by a call to <see cref="AddIdMapping" />.
        /// </param>
        /// <param name='progress'>
        /// Progress of the achievement. If the achievement is standard (not incremental), then
        /// a progress of 0.0 will reveal the achievement and 100.0 will unlock it. Behavior of other
        /// values is undefined. If the achievement is incremental, then this value is interpreted
        /// as the total percentage of the achievement's progress that the player should have
        /// as a result of this call (regardless of the progress they had before). So if the
        /// player's previous progress was 30% and this call specifies 50.0, the new progress will
        /// be 50% (not 80%).
        /// </param>
        /// <param name='callback'>
        /// Callback that will be called to report the result of the operation: <c>true</c> on
        /// success, <c>false</c> otherwise.
        /// </param>
        public void ReportProgress(string achievementID, double progress, Action<bool> callback) {
            if (!IsAuthenticated()) {
                Logger.e("ReportProgress can only be called after authentication.");
                if (callback != null) {
                    callback.Invoke(false);
                }
                return;
            }


            // map ID, if it's in the dictionary
            Logger.d("ReportProgress, " + achievementID + ", " + progress);
            achievementID = MapId(achievementID);

            // if progress is 0.0, we just want to reveal it
            if (progress < 0.000001) {
                Logger.d("Progress 0.00 interpreted as request to reveal.");
                mClient.RevealAchievement(achievementID, callback);
                return;
            }

            // figure out if it's a standard or incremental achievement
            bool isIncremental = false;
            int curSteps = 0, totalSteps = 0;
            Achievement ach = mClient.GetAchievement(achievementID);
            if (ach == null) {
                Logger.w("Unable to locate achievement " + achievementID);
                Logger.w("As a quick fix, assuming it's standard.");
                isIncremental = false;
            } else {
                isIncremental = ach.IsIncremental;
                curSteps = ach.CurrentSteps;
                totalSteps = ach.TotalSteps;
                Logger.d("Achievement is " + (isIncremental ? "INCREMENTAL" : "STANDARD"));
                if (isIncremental) {
                    Logger.d("Current steps: " + curSteps + "/" + totalSteps);
                }
            }

            // do the right thing depending on the achievement type
            if (isIncremental) {
                // increment it to the target percentage (approximate)
                Logger.d("Progress " + progress +
                        " interpreted as incremental target (approximate).");
                int targetSteps = (int)(progress * totalSteps);
                int numSteps = targetSteps - curSteps;
                Logger.d("Target steps: " + targetSteps + ", cur steps:" + curSteps);
                Logger.d("Steps to increment: " + numSteps);
                if (numSteps > 0) {
                    mClient.IncrementAchievement(achievementID, numSteps, callback);
                }
            } else {
                // unlock it!
                Logger.d("Progress " + progress + " interpreted as UNLOCK.");
                mClient.UnlockAchievement(achievementID, callback);
            }
        }

        /// <summary>
        /// Increments an achievement. This is a Play Games extension of the ISocialPlatform API.
        /// </summary>
        /// <param name='achievementID'>
        /// The ID of the achievement to increment. This can be a raw Google Play
        /// Games achievement ID (alphanumeric string), or an alias that was previously configured
        /// by a call to <see cref="AddIdMapping" />.
        /// </param>
        /// <param name='steps'>
        /// The number of steps to increment the achievement by.
        /// </param>
        /// <param name='callback'>
        /// The callback to call to report the success or failure of the operation. The callback
        /// will be called with <c>true</c> to indicate success or <c>false</c> for failure.
        /// </param>
        public void IncrementAchievement(string achievementID, int steps, Action<bool> callback) {
            if (!IsAuthenticated()) {
                Logger.e("IncrementAchievement can only be called after authentication.");
                if (callback != null) {
                    callback.Invoke(false);
                }
                return;
            }


            // map ID, if it's in the dictionary
            Logger.d("IncrementAchievement: " + achievementID + ", steps " + steps);
            achievementID = MapId(achievementID);
            mClient.IncrementAchievement(achievementID, steps, callback);
        }

        /// <summary>
        /// Not implemented yet. Calls the callback with an empty list.
        /// </summary>
        public void LoadAchievementDescriptions(Action<IAchievementDescription[]> callback) {
            Logger.w("PlayGamesPlatform.LoadAchievementDescriptions is not implemented.");
            if (callback != null) {
                callback.Invoke(new IAchievementDescription[0]);
            }
        }

        /// <summary>
        /// Not implemented yet. Calls the callback with an empty list.
        /// </summary>
        public void LoadAchievements(Action<IAchievement[]> callback) {
            Logger.w("PlayGamesPlatform.LoadAchievements is not implemented.");
            if (callback != null) {
                callback.Invoke(new IAchievement[0]);
            }
        }

        /// <summary>
        /// Creates an achievement object which may be subsequently used to report an
        /// achievement.
        /// </summary>
        /// <returns>
        /// The achievement object.
        /// </returns>
        public IAchievement CreateAchievement () {
            return new PlayGamesAchievement();
        }

        /// <summary>
        /// Reports a score to a leaderboard.
        /// </summary>
        /// <param name='score'>
        /// The score to report.
        /// </param>
        /// <param name='board'>
        /// The ID of the leaderboard on which the score is to be posted. This may be a raw
        /// Google Play Games leaderboard ID or an alias configured through a call to
        /// <see cref="AddIdMapping" />.
        /// </param>
        /// <param name='callback'>
        /// The callback to call to report the success or failure of the operation. The callback
        /// will be called with <c>true</c> to indicate success or <c>false</c> for failure.
        /// </param>
        public void ReportScore (long score, string board, Action<bool> callback) {
            if (!IsAuthenticated()) {
                Logger.e("ReportScore can only be called after authentication.");
                if (callback != null) {
                    callback.Invoke(false);
                }
                return;
            }

            Logger.d("ReportScore: score=" + score + ", board=" + board);
            string lbId = MapId(board);
            mClient.SubmitScore(lbId, score, callback);
        }

        /// <summary>
        /// Not implemented yet. Calls the callback with an empty list.
        /// </summary>
        public void LoadScores(string leaderboardID, Action<IScore[]> callback) {
            Logger.w("PlayGamesPlatform.LoadScores not implemented.");
            if (callback != null) {
                callback.Invoke(new IScore[0]);
            }
        }

        /// <summary>
        /// Not implemented yet. Returns null;
        /// </summary>
        public ILeaderboard CreateLeaderboard () {
            Logger.w("PlayGamesPlatform.CreateLeaderboard not implemented. Returning null.");
            return null;
        }

        /// <summary>
        /// Shows the standard Google Play Games achievements user interface,
        /// which allows the player to browse their achievements.
        /// </summary>
        public void ShowAchievementsUI () {
            if (!IsAuthenticated()) {
                Logger.e("ShowAchievementsUI can only be called after authentication.");
                return;
            }

            Logger.d("ShowAchievementsUI");
            mClient.ShowAchievementsUI();
        }

        /// <summary>
        /// Shows the standard Google Play Games leaderboards user interface,
        /// which allows the player to browse their leaderboards. If you have
        /// configured a specific leaderboard as the default through a call to
        /// <see cref="SetDefaultLeaderboardForUi" />, the UI will show that
        /// specific leaderboard only. Otherwise, a list of all the leaderboards
        /// will be shown.
        /// </summary>
        public void ShowLeaderboardUI () {
            if (!IsAuthenticated()) {
                Logger.e("ShowLeaderboardUI can only be called after authentication.");
                return;
            }
            Logger.d("ShowLeaderboardUI");
            mClient.ShowLeaderboardUI(MapId(mDefaultLbUi));
        }

        /// <summary>
        /// Shows the standard Google Play Games leaderboard UI for the given
        /// leaderboard.
        /// </summary>
        /// <param name='lbId'>
        /// The ID of the leaderboard to display. This may be a raw
        /// Google Play Games leaderboard ID or an alias configured through a call to
        /// <see cref="AddIdMapping" />.
        /// </param>
        public void ShowLeaderboardUI(string lbId) {
            if (!IsAuthenticated()) {
                Logger.e("ShowLeaderboardUI can only be called after authentication.");
                return;
            }
            Logger.d("ShowLeaderboardUI, lbId=" + lbId);
            if (lbId != null) {
                lbId = MapId(lbId);
            }
            mClient.ShowLeaderboardUI(lbId);
        }

        /// <summary>
        /// Sets the default leaderboard for the leaderboard UI. After calling this
        /// method, a call to <see cref="ShowLeaderboardUI" /> will show only the specified
        /// leaderboard instead of showing the list of all leaderboards.
        /// </summary>
        /// <param name='lbid'>
        /// The ID of the leaderboard to display on the default UI. This may be a raw
        /// Google Play Games leaderboard ID or an alias configured through a call to
        /// <see cref="AddIdMapping" />.
        /// </param>
        public void SetDefaultLeaderboardForUI(string lbid) {
            Logger.d("SetDefaultLeaderboardForUI: " + lbid);
            if (lbid != null) {
                lbid = MapId(lbid);
            }
            mDefaultLbUi = lbid;
        }

        /// <summary>
        /// Not implemented yet. Calls the callback with <c>false</c>.
        /// </summary>
        public void LoadFriends (ILocalUser user, Action<bool> callback) {
            Logger.w("PlayGamesPlatform.LoadFriends not implemented.");
            if (callback != null) {
                callback.Invoke(false);
            }
        }

        /// <summary>
        /// Not implemented yet. Calls the callback with <c>false</c>.
        /// </summary>
        public void LoadScores (ILeaderboard board, Action<bool> callback) {
            Logger.w("PlayGamesPlatform.LoadScores not implemented.");
            if (callback != null) {
                callback.Invoke(false);
            }
        }

        /// <summary>
        /// Not implemented yet. Returns false.
        /// </summary>
        public bool GetLoading (ILeaderboard board) {
            return false;
        }

        /// <summary>
        /// Sets the cloud cache encrypter. The cloud cache encrypter is a delegate that
        /// encrypts a byte buffer before it is saved to local storage, and decrypts the
        /// buffer after it's read from storage. It is only used in platforms where the
        /// cloud cache is implemented by the plugin as opposed to the underlying system
        /// (currently, only iOS).</summary>
        /// <param name='encrypter'>
        /// The encrypter delegate.
        /// </param>
        public void SetCloudCacheEncrypter(BufferEncrypter encrypter) {
            mClient.SetCloudCacheEncrypter(encrypter);
        }

        /// <summary>
        /// Loads app state (cloud save) data from the server.
        /// </summary>
        /// <param name='slot'>
        /// The app state slot number. The exact number of slots and their size can be seen
        /// in the Google Play Games documentation. Slot 0 is always available, and is at
        /// least 128K long.
        /// </param>
        /// <param name='callbacks'>
        /// The callbacks to call when the state is loaded, or when a conflict occurs.
        /// </param>
        public void LoadState(int slot, OnStateLoadedListener listener) {
            if (!IsAuthenticated()) {
                Logger.e("LoadState can only be called after authentication.");
                if (listener != null) {
                    listener.OnStateLoaded(false, slot, null);
                }
                return;
            }
            mClient.LoadState(slot, listener);
        }

        /// <summary>
        /// Writes app state (cloud save) data to the server.
        /// </summary>
        /// <param name='slot'>
        /// The app state slot number. The exact number of slots and their size can be seen
        /// in the Google Play Games documentation. Slot 0 is always available, and is at
        /// least 128K long.
        /// </param>
        /// <param name='data'>
        /// The data to write.
        /// </param>
        public void UpdateState(int slot, byte[] data, OnStateLoadedListener listener) {
            if (!IsAuthenticated()) {
                Logger.e("UpdateState can only be called after authentication.");
                if (listener != null) {
                    listener.OnStateSaved(false, slot);
                }
                return;
            }
            mClient.UpdateState(slot, data, listener);
        }

        /// <summary>
        /// Gets the local user.
        /// </summary>
        /// <returns>
        /// The local user.
        /// </returns>
        public ILocalUser localUser {
            get {
                return mLocalUser;
            }
        }

        /// Register an invitation delegate to be notified when a multiplayer invitation arrives
        public void RegisterInvitationDelegate(BasicApi.InvitationReceivedDelegate deleg) {
            mClient.RegisterInvitationDelegate(deleg);
        }

        private string MapId(string id) {
            if (id == null) {
                return null;
            }
            if (mIdMap.ContainsKey(id)) {
                string result = mIdMap[id];
                Logger.d("Mapping alias " + id + " to ID " + result);
                return result;
            }
            return id;
        }
    }
}

